#!/usr/bin/env node

/*
 * Builds with webpack and generates a Makefile include that
 * lists all dependencies, inputs, outputs, and installable files
 */

function fatal(message, code) {
    console.log("webpack-make: " + message);
    process.exit(code || 1);
}

var webpack, path, stdio, fs;

try {
    webpack = require("webpack");
    path = require("path");
    stdio = require("stdio");
    fs = require("fs");
} catch(ex) {
    fatal(ex.message, 127); /* missing looks for this */
}

var ops = stdio.getopt({
    config: { key: "c", args: 1, description: "Path to webpack.config.js", default: "webpack.config.js" },
    deps: { key: "d", args: 1, description: "Output dependencies in Makefile format" },
    watch: { key: "w", args: 0, description: "Enable webpack watch mode" },
});

var srcdir = process.env.SRCDIR || ".";
var makefile = ops.deps;
var prefix = "packages";
var npm = { "dependencies": { } };

if (makefile) {
    prefix = makefile.split("/").slice(-2, -1)[0];
    process.env["ONLYDIR"] = prefix + "/";
    npm = JSON.parse(fs.readFileSync(path.join(srcdir, "package.json"), "utf8"));
}

var cwd = process.cwd();
var config_path = path.resolve(cwd, ops.config);
var config = require(config_path);

// The latest input file time updated and used below
var latest = fs.statSync(config_path).mtime;

compiler = webpack(config);

if (ops.watch) {
    compiler.hooks.watchRun.tap("WebpackInfo", compilation => {
        const time = new Date().toTimeString().split(' ')[0];
        process.stdout.write(`${ time  } Build started\n`);
    });
    compiler.watch(config.watchOptions, process_result);
} else {
    compiler.run(process_result);
}

function process_result(err, stats) {
    // process.stdout.write(stats.toString({colors: true}) + "\n");

    if (err) {
        console.log(JSON.stringify(err));
        process.exit(1);
        return;
    }

    if (ops.watch) {
        const info = stats.toJson();
        const time = new Date().toTimeString().split(' ')[0];
        process.stdout.write(`${ time  } Build succeeded, took ${ info.time/1000 }s\n`);
    }

    // Failure exit code when compilation fails
    if (stats.hasErrors() || stats.hasWarnings()) {
        console.log(stats.toString("normal"));
        if (!ops.watch)
            process.exit(1);
        return;
    }

    if (makefile) {
        if (!ops.watch)
            generateDeps(makefile, stats);
        else {
            // Force "make" to re-create everything.  The results of
            // incremental building are not good enough for building
            // RPMs, for example.
            var stamp = makefile.split("/").slice(0, -1).concat(["stamp"]).join("/");
            if (fs.existsSync(makefile))
                fs.unlinkSync(makefile);
            if (fs.existsSync(stamp))
                fs.unlinkSync(stamp);
        }
    }
}

function generateDeps(makefile, stats) {

    // Note that these are cheap ways of doing a set
    var inputs = { };
    var po_locations = { };
    var outputs = { };
    var installs = { };
    var tests = { };
    var debugs = { };

    var pkgdir = path.dirname(makefile);
    var stampfile = pkgdir + '/stamp';

    stats.compilation.modules.forEach(function(module) {
        // skip external, multi, and other "not quite" modules
        if (module.constructor.name !== 'NormalModule')
            return;
        var parts = module.identifier().split("!");
        parts.concat(module.fileDependencies || []).forEach(function(part) {
            var input = part.split("?")[0];
            maybePushInput(inputs, po_locations, input);

            /* We distribute licenses, so treat them as inputs */
            moduleLicenses(input).forEach(function(input) {
                maybePushInput(inputs, { }, input);
            });
        });
    });

    stats.compilation.fileDependencies.forEach(function(file) {
        maybePushInput(inputs, po_locations, file);
        moduleLicenses(file).forEach(function(input) {
            maybePushInput(inputs, { }, input);
        });
    });

    // All the dependent files
    var asset, output;
    var now = Math.floor(Date.now() / 1000);

    // Strip builddir from output paths
    var dir = stats.compilation.outputOptions.path;
    if (dir.indexOf(cwd) === 0)
        dir = dir.substr(cwd.length+1);

    for(asset in stats.compilation.assets) {
        output = path.join(dir, asset);
        fs.utimesSync(output, now, now);

        /*
         * The manifest.json files are installed and built by Makefile.am.
         * When webpack is used on its own it *should* copy these files
         * (albeit poorly without substitution) ... but now that make is
         * in play lets have them generated by make
         */
        if (endsWith(output, "manifest.json"))
	    fs.unlinkSync(output);
	else
            outputs[output] = output;

	if (output.indexOf("/test-") !== -1 && endsWith(output, ".html")) {
            tests[output] = output;
            continue;
        }

        var install = output;
	if (!endsWith(output, "manifest.json") &&
            !endsWith(output, "override.json") &&
            !endsWith(output, "shell/index.html") &&
            !endsWith(output, ".png") &&
            !endsWith(output, ".map") &&
            !endsWith(output, ".ttf") &&
            !endsWith(output, ".woff") &&
            !endsWith(output, ".gif")) {
            install += ".gz";
        }

        // Debug output and tests gets installed separately
        if (endsWith(install, ".map"))
            debugs[install] = install;
        else if (output.indexOf("/test-") === -1)
            installs[install] = install;
    }

    // Finalize all the sets into arrays
    inputs = Object.keys(inputs).sort();
    po_locations = Object.keys(po_locations).sort();
    outputs = Object.keys(outputs).sort();
    installs = Object.keys(installs).sort();
    tests = Object.keys(tests).sort();
    debugs = Object.keys(debugs).sort();

    var lines = [ "# Generated Makefile data for " + prefix, "# Stamp: " + latest, "" ];

    function makeArray(name, values) {
        lines.push(name + " = \\");
        values.forEach(function(value) {
            lines.push("\t" + value + " \\");
        });
        lines.push("\t$(NULL)");
        lines.push("");
    }

    makeArray(prefix + "_INPUTS", inputs);
    makeArray(prefix + "_OUTPUTS", outputs);

    lines.push(prefix + "_PO = " +
               path.join(pkgdir, "po.js") +
               " $(patsubst %," + path.join(pkgdir, "po.%.js") + ",$(LINGUAS))");
    lines.push("");

    installs.push("$(addsuffix .gz,$(" + prefix + "_PO))");

    makeArray(prefix + "_INSTALL", installs);
    makeArray(prefix + "_DEBUG", debugs);
    makeArray(prefix + "_TESTS", tests);

    var filters = po_locations.map(function (l) { return "-N " + l; });
    filters.push("-N src/base1/cockpit.js"); // FIXME: See #13906
    if (prefix === "shell")
        filters.push("$(addprefix -N ,$(shell find pkg/ -name manifest.json.in))");

    lines.push(path.join(pkgdir, "%.po") + ": po/%.po");
    lines.push("\t$(AM_V_GEN) $(MKDIR_P) $(dir $@) && \\");
    lines.push("\t$(MSGGREP) " + filters.join(" ") + " $< > $@.tmp && mv $@.tmp $@");
    lines.push("");

    lines.push(stampfile + ": $(" + prefix + "_INPUTS)");
    lines.push("");

    outputs.forEach(function(name) {
        lines.push(name + ": " + stampfile);
        lines.push("")
    });

    inputs.forEach(function(name) {
        lines.push(name + ":");
        lines.push("")
    });

    lines.push("WEBPACK_INPUTS += $(" + prefix + "_INPUTS)");
    lines.push("WEBPACK_OUTPUTS += $(" + prefix + "_OUTPUTS)");
    lines.push("WEBPACK_PO += $(" + prefix + "_PO)");
    lines.push("WEBPACK_INSTALL += $(" + prefix + "_INSTALL)");
    lines.push("WEBPACK_DEBUG += $(" + prefix + "_DEBUG)");
    lines.push("TESTS += $(" + prefix + "_TESTS)");
    lines.push("");

    lines.push(prefix + ": " + stampfile);
    lines.push("maintainer-clean-" + prefix + ": ");
    lines.push("\trm -rf $(" + prefix + "_OUTPUTS) $(" + prefix + "_INSTALL) " + stampfile);
    lines.push("maintainer-clean-local:: clean-" + prefix);

    data = lines.join("\n") + "\n";
    fs.writeFileSync(makefile, data);
}

function moduleLicenses(input) {
    var parts = input.split(path.sep);
    var pos = parts.indexOf("node_modules");
    var directory, results = [];
    if (pos !== -1) {
        directory = parts.slice(0, pos + 2).join(path.sep);
        fs.readdirSync(directory).forEach(function(name) {
            if (name.indexOf("COPYING") !== -1 || name.indexOf("LICENSE") !== -1 ||
                name.indexOf("README.md") !== -1 || name.indexOf("package.json") !== -1)
                results.push(path.join(directory, name));
        });
    }

    return results;
}

function maybePushInput(inputs, po_locations, input) {
    var po_location;

    // Don't include or external refs
    if (endsWith(input, '/') || endsWith(input, 'manifest.json.in') ||
        input.indexOf("external ") === 0 || input.indexOf("multi ") === 0) {
        return;
    }

    // Don't include node devDependencies
    var parts = input.split(path.sep);
    var pos = parts.indexOf("node_modules");
    if (pos !== -1) {
        deps = npm["dependencies"] || { };
        have = parts[pos + 1] in deps;
        if (!have)
            return;
    }

    // The latest modified date
    var stats = fs.statSync(input);
    if (stats.mtime > latest)
        latest = stats.mtime;

    // Strip builddir and srcdir absolute paths from input file and add it
    if (input.indexOf(cwd) === 0)
        input = input.substr(cwd.length+1);
    if (srcdir && input.indexOf(srcdir) === 0) {
        po_location = input.substr(srcdir.length+1);
        input = input.substr(srcdir.length+1);
    } else {
        po_location = input;
    }

    po_locations[po_location] = true;
    inputs[input] = true;
}

function endsWith(string, suffix) {
    return (string.lastIndexOf(suffix) === (string.length - suffix.length))
}
